//This file is a part of StoragePlug for Minecraft.
//http://code.google.com/p/minecraft-storageplug/
//StoragePlug is Copyright (C) 2011 Christopher Smith
//Released under the MIT license:
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights 
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
//DEALINGS IN THE SOFTWARE.
//
//Related to Minecraft Copyright (C) 2009-2011 Mojang http://www.minecraft.net/

package net.minecraft.storageplug.sqlite;

import java.io.File;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import net.minecraft.storageplug.Key;
import org.w3c.dom.Element;

import com.almworks.sqlite4java.SQLite;
import com.almworks.sqlite4java.SQLiteConnection;
import com.almworks.sqlite4java.SQLiteConstants;
import com.almworks.sqlite4java.SQLiteException;
import com.almworks.sqlite4java.SQLiteStatement;

import java.util.AbstractMap.SimpleEntry;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.storageplug.DeflateStreamStorageAdapter;
import net.minecraft.storageplug.StorageProvider;
import net.minecraft.storageplug.StreamStorageAdapter;
import net.minecraft.storageplug.StreamStorageProvider;

public class SQLiteStorageProvider
	implements StreamStorageProvider
{
	private static class Link
	{
		public final SQLiteConnection conn;
		
        public final SQLiteStatement beginTrans;
        public final SQLiteStatement endTrans;
        public final SQLiteStatement cancelTrans;
        public final SQLiteStatement checkpoint;
        
		public final SQLiteStatement readWorldInfo;
		public final SQLiteStatement writeWorldInfo;
		public final SQLiteStatement readItemData;
		public final SQLiteStatement writeItemData;
		public final SQLiteStatement readChunk;
		public final SQLiteStatement writeChunk;
		public final SQLiteStatement readPlayer;
		public final SQLiteStatement writePlayer;
        
		public Link(File f) throws Exception
		{
			conn = new SQLiteConnection(f);
			
			conn.openV2(
				SQLiteConstants.SQLITE_OPEN_CREATE |
				SQLiteConstants.SQLITE_OPEN_READWRITE |
				SQLiteConstants.SQLITE_OPEN_SHAREDCACHE);
                        
            conn.exec("PRAGMA journal_mode=WAL;");
            conn.exec("PRAGMA synchronous=NORMAL;");
            conn.exec("PRAGMA read_uncommitted=TRUE");            
			
            beginTrans = conn.prepare("BEGIN IMMEDIATE");
            endTrans = conn.prepare("END TRANSACTION");
            cancelTrans = conn.prepare("ROLLBACK TRANSACTION");
            checkpoint = conn.prepare("PRAGMA wal_checkpoint(FULL)");
            
			readWorldInfo = conn.prepare("SELECT nbt FROM world");
			writeWorldInfo = conn.prepare("INSERT OR REPLACE INTO world(key, nbt) VALUES(0, ?)");
			readItemData = conn.prepare("SELECT nbt FROM itemdata WHERE name = ?");
			writeItemData = conn.prepare("INSERT OR REPLACE INTO itemdata(nbt, name) VALUES(?,?)");
			readChunk = conn.prepare("SELECT nbt FROM chunk WHERE dim = ? AND x = ? AND z = ?");
			writeChunk = conn.prepare("INSERT OR REPLACE INTO chunk(nbt, dim, x, z) VALUES(?,?,?,?)");
			readPlayer = conn.prepare("SELECT nbt FROM player WHERE name = ?");
			writePlayer = conn.prepare("INSERT OR REPLACE INTO player(nbt, name) VALUES(?,?)");
		}
		
		public void dispose()
		{
			readWorldInfo.dispose();
			writeWorldInfo.dispose();
			readItemData.dispose();
			writeItemData.dispose();
			readChunk.dispose();
			writeChunk.dispose();
			readPlayer.dispose();
			writePlayer.dispose();
		}
	}

    private final File dbfile;
    private final ConcurrentHashMap<Thread, Link> pool;
    private final Object writeLock = new Object();//hold during write to avoid "database/table locked" messages
        
    Link getLink()
        throws Exception
    {
        Thread currentThread = Thread.currentThread();
        Link link = null;
        
        if(pool.containsKey(currentThread))
        {
            link = pool.get(currentThread);
        }
        else
        {
            link = new Link(dbfile);
            pool.put(currentThread, link);
        }
        
        return link;
    }
	
	public SQLiteStorageProvider(File dir)
		throws Exception
	{		
		dir.mkdirs();
		dbfile = new File(dir, "world.sqlite");
		boolean already_exists = dbfile.isFile();
		SQLite.setSharedCache(true);
				
		SQLiteConnection conn = new SQLiteConnection(dbfile);
		
		try
		{		
			conn.openV2(
				SQLiteConstants.SQLITE_OPEN_CREATE |
				SQLiteConstants.SQLITE_OPEN_READWRITE |
                SQLiteConstants.SQLITE_OPEN_PRIVATECACHE);

			if(!already_exists)
				CreateTables(conn);
			else
			{
				// check to see if this is a supported version
				SQLiteStatement get_version = conn.prepare("SELECT version FROM dbinfo");
				try
				{
					get_version.step();
					int version = get_version.columnInt(0);
					if(version != 1000)
						throw new RuntimeException("Invalid Minecraft SQLite database version. Expected 1000, got " + version + ". Update StoragePlug SQLite to latest version.");
				} finally
				{
					get_version.dispose();
				}
			}
		}
		finally
		{
			conn.dispose();
		}
		
		pool = new ConcurrentHashMap<Thread, Link>(4);
	}
	
	public static StorageProvider createInstance(File storagedir, Element config, ClassLoader loader)
        throws Exception
	{
		return new StreamStorageAdapter(new DeflateStreamStorageAdapter(new SQLiteStorageProvider(storagedir)));
	}

	private void CreateTables(SQLiteConnection conn)
		throws SQLiteException
	{
		conn.exec(
			"CREATE TABLE world" +
			"(" +
			"	key INTEGER PRIMARY KEY," +				
			"	nbt NONE" +
			")");
		
		conn.exec(
			"CREATE TABLE player" +
			"(" +
			"	name TEXT PRIMARY KEY," +
			"	nbt NONE" +
			")");
		
		conn.exec(
			"CREATE TABLE chunk" +
			"(" +
			"	dim INTEGER," +
			"	x INTEGER," +
			"	z INTEGER," +
			"	nbt NONE," +
			"	PRIMARY KEY(dim, x, z)" +
			")");
		
		conn.exec(
			"CREATE TABLE itemdata" +
			"(" +
			"	name TEXT PRIMARY KEY," +
			"	nbt NONE" +
			")");
		
		conn.exec(
			"CREATE TABLE uniqueid" +
			"(" +
			"	name TEXT PRIMARY KEY," +
			"	lastval INTEGER" +
			")");
		
		conn.exec(
			"CREATE TABLE dbinfo" +
			"(" +
			"	created_time INTEGER," +
			"	opened_time INTEGER," +
			"	version INTEGER" +
			")");

		//insert starting dbinfo values
		SQLiteStatement init_dbinfo = conn.prepare("INSERT INTO dbinfo VALUES (?, ?, ?)");
		try
		{
			init_dbinfo.bind(1, (new Date()).getTime());
			init_dbinfo.bind(2, (new Date()).getTime());
			init_dbinfo.bind(3, 1000);
			init_dbinfo.stepThrough();
		} finally
		{
			init_dbinfo.dispose();
		}
	}
    
    void readToStream(Link link, SQLiteStatement stmt, ReadOperation op)
        throws Exception
    {
        try
        {
            if(stmt.step())
            {
                op.read(stmt.columnStream(0));
            }
        }
        finally
        {
            stmt.reset();
        }
    }
    
    void writeFromStream(Link link, SQLiteStatement stmt, WriteOperation op)
        throws Exception
    {
        try
        {
            OutputStream stream = stmt.bindStream(1);
            op.write(stream);
            stmt.stepThrough();
        }
        finally
        {
            stmt.reset();
        }
    }

    @Override
    public void readDataStream(ReadOperation op)
        throws Exception
    {
        readDataStream(op, getLink());
    }
    
    @Override
    public void writeDataStream(WriteOperation op)
        throws Exception
    {
        synchronized(writeLock)
        {
            Link link = getLink();

            writeDataStream(op, getLink());

            link.checkpoint.stepThrough().reset();
        }
    }
    
    private void readDataStream(ReadOperation op, Link link)
        throws Exception
    {
        Key key = op.key();
        
        if(key instanceof Key.WorldInfo)
        {
    		readToStream(link, link.readWorldInfo, op);
        }
        else if(key instanceof Key.Player)
        {
            Key.Player key2 = (Key.Player)key;
            
            link.readItemData.bind(1, key2.name);
            readToStream(link, link.readPlayer, op);
        }
        else if(key instanceof Key.ItemData)
        {
            Key.ItemData key2 = (Key.ItemData)key;

            link.readItemData.bind(1, key2.name);
            readToStream(link, link.readItemData, op);   
        }
        else if(key instanceof Key.Chunk)
        {
            Key.Chunk key2 = (Key.Chunk)key;
            
            link.readChunk
                .bind(1, key2.dimension)
                .bind(2, key2.x)
                .bind(3, key2.z);

            readToStream(link, link.readChunk, op);
        }
    }
    
    private void writeDataStream(WriteOperation op, Link link)
        throws Exception
    {
        Key key = op.key();
        
        if(key instanceof Key.WorldInfo)
        {
    		writeFromStream(link, link.writeWorldInfo, op);
        }
        else if(key instanceof Key.Player)
        {
            Key.Player key2 = (Key.Player)key;
            
            link.writeItemData.bind(2, key2.name);
            writeFromStream(link, link.writePlayer, op);
        }
        else if(key instanceof Key.ItemData)
        {
            Key.ItemData key2 = (Key.ItemData)key;

            link.writeItemData.bind(2, key2.name);
            writeFromStream(link, link.writeItemData, op);
        }
        else if(key instanceof Key.Chunk)
        {
            Key.Chunk key2 = (Key.Chunk)key;
            
            link.writeChunk
                .bind(2, key2.dimension)
                .bind(3, key2.x)
                .bind(4, key2.z);

            writeFromStream(link, link.writeChunk, op);
        }
    }

    @Override
    public void readDataStreamBatch(Collection<? extends ReadOperation> list)
        throws Exception
    {
        Link link = getLink();
        boolean successful = false;
        
        link.beginTrans.stepThrough().reset();
        
        try
        {
            for(ReadOperation op : list)
                readDataStream(op, link);
            successful = true;
        }
        finally
        {
            if(successful)
            {
                link.endTrans.stepThrough().reset();
                link.checkpoint.stepThrough().reset();
            }
            else
                link.cancelTrans.stepThrough().reset();
        }
    }
    
    @Override
    public void writeDataStreamBatch(Collection<? extends WriteOperation> list)
        throws Exception
    {
        synchronized(writeLock)
        {
            while(true)
            {
                Link link = getLink();
                boolean successful = false;

                link.beginTrans.stepThrough().reset();

                try
                {
                    for(WriteOperation op : list)
                        writeDataStream(op, link);
                    successful = true;
                }
                finally
                {
                    if(successful)
                        link.endTrans.stepThrough().reset();
                    else
                        link.cancelTrans.stepThrough().reset();
                }

                break;
            }
        }
    }

	@Override
	public void flush()
	{
		//no interface to flush, do nothing
	}

	@Override
	public void dispose()
	{
		for(Link l : pool.values())
			l.dispose();
	}

	@Override
	synchronized public int nextUniqueID(String key)
		throws Exception
	{
        synchronized(writeLock)
        {
            Link link = getLink();
            SQLiteConnection conn = link.conn;

            SQLiteStatement stmt = null;
            boolean successful = false;

            try
            {
                link.beginTrans.stepThrough().reset();

                stmt = conn.prepare("SELECT lastval FROM uniqueid WHERE name = ?");
                stmt.bind(1, key);

                if(stmt.step())
                {
                    int value = stmt.columnInt(0) + 1;

                    stmt.dispose();
                    stmt = null;

                    stmt = conn.prepare("UPDATE uniqueid SET lastval = ? WHERE name = ?");

                    stmt.bind(1, value + 1);
                    stmt.bind(2, key);

                    stmt.stepThrough();

                    return value;
                }
                else
                {
                    stmt.dispose();
                    stmt = null;

                    stmt = conn.prepare("INSERT INTO uniqueid VALUES(?, ?)");

                    stmt.bind(1, key);
                    stmt.bind(2, 0); //start at 0

                    stmt.stepThrough();

                    return 0;
                }
            }
            finally
            {
                if(stmt != null)
                    stmt.dispose();

                if(successful)
                    link.endTrans.stepThrough().reset();
                else
                    link.cancelTrans.stepThrough().reset();
            }
        }
	}
    
    @Override
    public Collection<? extends Key> iterKeys(Class keytype)
        throws Exception
    {
        Link link = getLink();
        SQLiteConnection conn = link.conn;
        SQLiteStatement stmt = null;
        
        try
        {
            if(keytype.equals(Key.WorldInfo.class))
            {
                List<Key.WorldInfo> list = new ArrayList<Key.WorldInfo>(1);
                list.add(new Key.WorldInfo());
                return list;
            }
            else if (keytype.equals(Key.Player.class))
            {
                List<Key.Player> list = new ArrayList<Key.Player>(1);

                stmt = conn.prepare("SELECT name FROM player");

                while(stmt.step())
                    list.add(new Key.Player(stmt.columnString(0)));

                return list;
            }
            else if (keytype.equals(Key.ItemData.class))
            {
                List<Key.ItemData> list = new ArrayList<Key.ItemData>(1);

                stmt = conn.prepare("SELECT name FROM itemdata");

                while(stmt.step())
                    list.add(new Key.ItemData(stmt.columnString(0)));

                return list;
            }
            else if (keytype.equals(Key.Chunk.class))
            {
                List<Key.Chunk> list = new ArrayList<Key.Chunk>(1);

                stmt = conn.prepare("SELECT dim, x, z FROM chunk");

                while(stmt.step())
                    list.add(new Key.Chunk(stmt.columnInt(0), stmt.columnInt(1), stmt.columnInt(2)));

                return list;
            }
        }
        finally
        {
            if(stmt != null)
                stmt.dispose();
        }

        return null;
    }

	@Override
	public Collection<? extends Map.Entry<String, Integer>> readUniqueIDs()
		throws Exception
	{
		Link link = getLink();
		SQLiteConnection conn = link.conn;
		SQLiteStatement stmt = null;
		ArrayList<SimpleEntry<String, Integer>> map = new ArrayList<SimpleEntry<String, Integer>>(64);
		
		try
		{
			stmt = conn.prepare("SELECT name, lastval FROM uniqueid");
		
			while(stmt.step())
				map.add(new SimpleEntry<String, Integer>(stmt.columnString(0), stmt.columnInt(1)));
		}
		finally
		{
			if(stmt != null)
				stmt.dispose();
		}
		
		return map;
	}

	@Override
	public void writeUniqueIDs(Collection<? extends Map.Entry<String, Integer>> map)
		throws Exception
	{
        synchronized(writeLock)
        {
            Link link = getLink();
            SQLiteConnection conn = link.conn;
            SQLiteStatement stmt = null;

            try
            {		
                stmt = conn.prepare("INSERT OR REPLACE INTO uniqueid(name, lastval) VALUES(?,?)");

                for(Entry<String, Integer> i : map)
                {
                    stmt.bind(1, i.getKey());
                    stmt.bind(2, i.getValue());

                    stmt.stepThrough();
                }
            }
            finally
            {
                if(stmt != null)
                    stmt.dispose();
            }
        }
	}
}
